package br.perfiman.service.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.ejb.EJB;
import javax.ejb.Stateless;

import br.perfiman.dao.AbstractDAOService;
import br.perfiman.dao.TradeDAOService;
import br.perfiman.model.Portfolio;
import br.perfiman.model.Trade;
import br.perfiman.service.AssetService;
import br.perfiman.service.BrokerService;
import br.perfiman.service.PortfolioService;
import br.perfiman.service.TradeService;
import br.perfiman.service.impl.patterns.strategy.brokerage.BrokerageStrategy;
import br.perfiman.utils.BussinessUtils;

/**
 *
 * @author Wagner Costa
 * @since 27/12/2007
 * @version 1.0
 *
 */
@Stateless
public class TradeServiceBean extends AbstractServiceBean<Trade> implements TradeService{
	@EJB
	protected TradeDAOService dao;
	
	@EJB
	protected PortfolioService portfolioService;
	
	@EJB
	protected BrokerService brokerService;
	
	@EJB
	protected AssetService assetService;
	
	private List<Trade> trades = new ArrayList<Trade>();
	private List<Trade> blackList;
	private List<TradesLeft> buyOperations, sellOperations;
	private List<Exchanges> exchanges;
	private Trade dad, brother;
	private TradesLeft left;
	private Double averagePrice;
	
	@Override
	public AbstractDAOService<Trade> getDao() {
		return dao;
	}

	@Override
	public Trade insert(Trade entity) throws Exception {
		//checking if not exists a trade new than this one to be shifted
		List<Trade> newTrades = dao.getNewTrades(entity);
		if(newTrades == null || newTrades.isEmpty())
			return insertTrade(entity);
		
		List<Trade> parentTrades = new ArrayList<Trade>();
		for(int i = 0; i < newTrades.size(); i++){
			Trade t = newTrades.get(i);
			//The parent trade must be removed after all simple/child trades to avoid persistence database problems
			if(t.getOperationType().equals(Trade.OPERATION_TYPE_MIX)){
				parentTrades.add(t);
				continue;
			}
			remove(t);
			t.setId(null);
			//removing the mixed operation children
			if(t.getParent() != null){
				newTrades.remove(i--);
			}
		}
		//removing the parent trade
		for(Trade t: parentTrades){
			remove(t);
			t.setId(null);
		}
		Trade returnTrade = insertTrade(entity);
		for(Trade t: newTrades){
			insertTrade(t);
		}
		return returnTrade;
	}
	
	@SuppressWarnings("unused")
	private void reinsertValues() throws Exception{
		List<Trade> newTrades = dao.retrieveAll();
		
		List<Trade> parentTrades = new ArrayList<Trade>();
		for(int i = 0; i < newTrades.size(); i++){
			Trade t = newTrades.get(i);
			//The parent trade must be removed after all simple/child trades to avoid persistence database problems
			if(t.getOperationType().equals(Trade.OPERATION_TYPE_MIX)){
				parentTrades.add(t);
				continue;
			}
			remove(t);
			t.setId(null);
			//removing the mixed operation children
			if(t.getParent() != null){
				newTrades.remove(i--);
			}
		}
		//removing the parent trade
		for(Trade t: parentTrades){
			remove(t);
			t.setId(null);
		}
		for(Trade t: newTrades){
			insertTrade(t);
		}
	}
	
	private Trade insertTrade(Trade entity) throws Exception {
		//persisting the asset
		entity.setAsset(assetService.retrieveById(entity.getAsset().getId()));
		
		//persisting the broker
		entity.setBroker(brokerService.retrieveById(entity.getBroker().getId()));

		Portfolio portfolio = portfolioService.retrieveByAsset(entity.getAsset());
		averagePrice = (portfolio == null)? 0d: portfolio.getAveragePrice();
		
		Trade returnTrade = null;
		if(isDayTrade(entity)){
			trades.add(entity);
			calculateDayTrade(portfolio);
			for(TradesLeft t: buyOperations){
				if(t.trade.getId() != null)
					dao.update(t.trade);
				else
					dao.insert(t.trade);
			}
			for(TradesLeft t: sellOperations){
				if(t.trade.getId() != null)
					dao.update(t.trade);
				else
					dao.insert(t.trade);
			}
			for(Trade t: blackList){
				remove(t);
			}
			returnTrade = entity;
		} else {
			entity.setOperationType(Trade.OPERATION_TYPE_COMMON);
			calculateTradeValues(entity, averagePrice);
			returnTrade = super.insert(entity);
		}
		
		portfolioService.updatePortfolio();
		return returnTrade;
	}
	
	/**
	 * Tell if the trade is inside a daytrade operation or not.
	 * Also fill all the daytrade operation of the day with the asset in the 'trades' variable
	 * 
	 * @author Wagner Costa
	 * @since 27/03/2008
	 * @version 1.0
	 * @param trade
	 * @return
	 * @throws Exception
	 */
	private boolean isDayTrade(Trade trade) throws Exception {
		trades = dao.getTradesByDate(trade.getAsset(), trade.getDate());
		if(trades == null || trades.isEmpty())
			return false;
		return true;
	}

	private void calculateDayTrade(Portfolio portfolio) throws Exception {
		//split between buy and sell operations
		buyOperations = new ArrayList<TradesLeft>();
		sellOperations = new ArrayList<TradesLeft>();
		blackList = new ArrayList<Trade>();
		for(Trade t: trades){
			t.setOperationType(Trade.OPERATION_TYPE_COMMON);
			if(t.getParent() != null){
				blackList.add(t);
				continue;
			}
			if(t.getType().equals(Trade.TYPE_BUY)) {
				buyOperations.add(new TradesLeft(t, t.getQuantity()));
			} else {
				sellOperations.add(new TradesLeft(t, t.getQuantity()));
			}
		}
		
		//make the exchanges between trades
		exchanges = new ArrayList<Exchanges>();
		TradesLeft tlBuy = null;
		TradesLeft tlSell = null;
		while((tlBuy = getNext(buyOperations)) != null){
			tlSell = getNext(sellOperations);
			if(tlSell == null)
				break;
			exchanges.add(exchange(tlBuy, tlSell));
		}
		
		//check any incomplete buy operation
		left = getIncomplete();
		if(left != null && left.trade.getType().equals(Trade.TYPE_BUY)){
			splitTrade(null);
			buyOperations.add(new TradesLeft(dad, 0));
			buyOperations.add(new TradesLeft(brother, 0));
		}
		
		//calculates the operation taxes
		for(TradesLeft tl: buyOperations){
			if(tl.qtyLeft > 0)
				break;
			tl.trade.setOperationType(Trade.OPERATION_TYPE_DAYTRADE);
			calculateTradeValues(tl.trade, 0d);
		}
		
		//calculates the sell operation taxes, using the buy price
		for(TradesLeft tl: sellOperations){
			if(tl.qtyLeft > 0)
				break;
			tl.trade.setOperationType(Trade.OPERATION_TYPE_DAYTRADE);
			List<Exchanges> exchangesTrade = getExchangeOperations(tl, exchanges);
			if(exchangesTrade.size() == 1){
				calculateTradeValues(tl.trade, BussinessUtils.getAveragePrice(exchangesTrade.get(0).tlb.trade));
				continue;
			}
			List<Trade> tradesList = new ArrayList<Trade>();
			for(Exchanges ex: exchangesTrade)
				tradesList.add(ex.tlb.trade);
			calculateTradeValues(tl.trade, BussinessUtils.getAveragePrice(tradesList));
		}
		
		//check any incomplete sell operation
		if(left != null && left.trade.getType().equals(Trade.TYPE_SELL)){
			splitTrade(portfolio);
			sellOperations.add(new TradesLeft(dad, 0));
			sellOperations.add(new TradesLeft(brother, 0));
		}
		calculateUntoucheds();
	}

	private void splitTrade(Portfolio portfolio) {
		try {
			dad = (Trade) left.trade.clone();
			brother = (Trade) left.trade.clone();
			
			dad.setOperationType(Trade.OPERATION_TYPE_MIX);
			brother.setOperationType(Trade.OPERATION_TYPE_COMMON);
			left.trade.setOperationType(Trade.OPERATION_TYPE_DAYTRADE);
			
			brother.setQuantity(left.qtyLeft);
			left.trade.setQuantity(dad.getQuantity() - left.qtyLeft);
			left.qtyLeft = 0;
			
			//applied to buy operations
			if(portfolio == null){
				calculateTradeValues(left.trade, 0d);
				calculateTradeValues(brother, 0d);
				calculateTradeValues(dad, 0d);
			//applied to sell operations
			} else {
				List<Exchanges> exchangesTrade = getExchangeOperations(left, exchanges);
				if(exchangesTrade.size() == 1){
					calculateTradeValues(left.trade, BussinessUtils.getAveragePrice(exchangesTrade.get(0).tlb.trade));
				} else {
					List<Trade> tradesList = new ArrayList<Trade>();
					for(Exchanges ex: exchangesTrade)
						tradesList.add(ex.tlb.trade);
					calculateTradeValues(left.trade, BussinessUtils.getAveragePrice(tradesList));
				}		
				calculateTradeValues(brother, portfolio.getAveragePrice());
				calculateTradeValues(dad, ((left.trade.getBuyAveragePrice() * left.trade.getQuantity()) + (brother.getBuyAveragePrice() * brother.getQuantity()))
							/ (left.trade.getQuantity() + brother.getQuantity()));
			}
			
			dad.setIrs(left.trade.getIrs() + brother.getIrs());
			dad.setFees(left.trade.getFees() + brother.getFees());
			left.trade.setBrokerage(getProportionalBrokerage(left.trade, dad.getQuantity()));
			brother.setBrokerage(getProportionalBrokerage(brother, dad.getQuantity()));
			left.trade.setParent(dad);
			brother.setParent(dad);
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
	}

	private Double getProportionalBrokerage(Trade trade, Integer quantity) {
		return trade.getQuantity() * trade.getBrokerage() / quantity;
	}

	private TradesLeft getIncomplete() {
		for(TradesLeft t: buyOperations)
			if(t.qtyLeft > 0 && !t.qtyLeft.equals(t.trade.getQuantity()))
				return t;
		for(TradesLeft t: sellOperations)
			if(t.qtyLeft > 0 && !t.qtyLeft.equals(t.trade.getQuantity()))
				return t;
		return null;
	}
	
	private void calculateUntoucheds() {
		for(TradesLeft t: buyOperations)
			if(t.qtyLeft.equals(t.trade.getQuantity())){
				t.trade.setOperationType(Trade.OPERATION_TYPE_COMMON);
				calculateTradeValues(t.trade, 0d);
				t.qtyLeft = 0;
			}
		for(TradesLeft t: sellOperations)
			if(t.qtyLeft.equals(t.trade.getQuantity())){
				t.trade.setOperationType(Trade.OPERATION_TYPE_COMMON);
				calculateTradeValues(t.trade, averagePrice);
				t.qtyLeft = 0;
			}
	}

	private List<Exchanges> getExchangeOperations(TradesLeft tl, List<Exchanges> exchanges) {
		List<Exchanges> ret = new ArrayList<Exchanges>();
		for(Exchanges ex: exchanges){
			if(ex.tls == tl)
				ret.add(ex);
		}
		return ret;
	}

	private Exchanges exchange(TradesLeft tlBuy, TradesLeft tlSell) {
		Exchanges ret = null;
		if(tlBuy.qtyLeft >= tlSell.qtyLeft){
			ret = new Exchanges(tlBuy, tlSell, tlSell.qtyLeft);
			tlBuy.qtyLeft -= tlSell.qtyLeft;
			tlSell.qtyLeft = 0;
		} else {
			ret = new Exchanges(tlBuy, tlSell, tlBuy.qtyLeft);
			tlSell.qtyLeft -= tlBuy.qtyLeft;
			tlBuy.qtyLeft = 0;
		}
		return ret;
	}

	private TradesLeft getNext(List<TradesLeft> list){
		for(TradesLeft tl: list){
			if(tl.qtyLeft > 0)
				return tl;
		}
		return null;
	}
	
	private void calculateTradeValues(Trade entity, Double averagePrice) {
		BrokerageStrategy brokerageStrategy = BrokerageStrategy.getStrategy(entity.getBroker());
		entity.setBrokerage(brokerageStrategy.getBrokerage(entity));
		entity.setFees(BussinessUtils.getFee(entity));
		entity.setIrs(0d);
		
		//calculating sell's values
		if(entity.getType().equals(Trade.TYPE_SELL)){
			entity.setBuyAveragePrice(averagePrice);
			entity.setProfit(BussinessUtils.getProfit(entity));
			entity.setIrs(BussinessUtils.getIRS(entity));
		}
	}

	@Override
	public void remove(Trade entity) throws Exception {
		super.remove(entity);
		portfolioService.updatePortfolio();
	}

	@Override
	public List<Trade> retrieveAll() throws Exception {
		return super.retrieveAll();
	}

	@Override
	public Trade retrieveById(Object id) throws Exception {
		return super.retrieveById(id);
	}

	@Override
	public Trade update(Trade entity) throws Exception {
		Trade ret = super.update(entity);
		portfolioService.updatePortfolio();
		return ret;
	}

	@Override
	public void validateBussinessRules(Trade entity, boolean isUpdate) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void validateInvalidFields(Trade entity, boolean isUpdate) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void validateRequiredFields(Trade entity, boolean isUpdate) {
		
	}
	
	private class TradesLeft{
		Trade trade;
		Integer qtyLeft;

		public TradesLeft(Trade trade, Integer qtyLeft) {
			this.trade = trade;
			this.qtyLeft = qtyLeft;
		}
	}
	
	private class Exchanges{
		TradesLeft tlb;
		TradesLeft tls;
		Integer qty;
		
		public Exchanges(TradesLeft tlb, TradesLeft tls, Integer qty) {
			this.tlb = tlb;
			this.tls = tls;
			this.qty = qty;
		}
	}

	public List<Trade> findByExample(Trade trade) throws Exception {
		return dao.findByExample(trade);
	}

	public List<Trade> retrieveTradesByPeriod(Date initialDate, Date finalDate, Integer Type) {
		return dao.retrieveTradesByPeriod(initialDate, finalDate, Type);
	}
	
	public Double getMonthsProfit(Integer month, Integer year) {
		Date[] range = BussinessUtils.getMonthRange(month, year);
		List<Trade> trades = retrieveTradesByPeriod(range[0], range[1], TradeService.TRADES_TYPE_SELL);
		Double profit = 0d;
		for(Trade trade: trades){
			if(trade.getOperationType().equals(Trade.OPERATION_TYPE_MIX))
				continue;
			profit += trade.getProfit();
		}
		return profit;
	}
}